iT邦幫忙

2026 iThome 鐵人賽

DAY 2
0
AI 自動化

讓 Claude 學會看圖:30 天打造 LLM 驅動的機器視覺工作站(MCP × OpenCV × Terminal Plugin)》系列 第 3

Day 3|三欄式佈局:Sidebar / Editor / Terminal,與 inline style 的戰爭

  • 分享至 

  • xImage
  •  

Day 3|三欄式佈局:Sidebar / Editor / Terminal,與 inline style 的戰爭

Day 3 封面:用藍圖精確切分螢幕

昨天講到 decorateHyper 把三欄佈局「包」在 Hyper 外面,以及 CSS 大戰打不贏、必須直接動 DOM 的結論。今天把整套佈局系統拆開來看:尺寸怎麼算、什麼時機動 DOM,以及一個讓我 debug 到懷疑人生的隱形殺手。

尺寸:全部用 calc() 從視窗寬度推導

三欄的規格是:Sidebar 固定 260px,Editor 佔剩餘寬度的 35%,Terminal 佔剩餘的 65%,底部再讓出 24px 給 StatusBar:

三欄尺寸全部用 calc() 推導
關鍵決策:寬度用 CSS calc() 表達,而不是用 JS 算好的像素值:

// src/index.js — updateLayout()
const termLeft = editorOpen
  ? `calc(${SIDEBAR_WIDTH}px + (100vw - ${SIDEBAR_WIDTH}px) * 0.35)`
  : `${SIDEBAR_WIDTH}px`;

如果寫死像素,使用者一拉視窗大小佈局就爛了,你得監聽 resize 事件重算。交給 calc(100vw ...),瀏覽器在每次 reflow 自動重算——整個插件完全沒有 resize listener,這是「讓平台幫你工作」的典型例子。

選擇器的小陷阱:CSS Modules 的雜湊類名

Hyper 的 UI 用 CSS Modules,類名在 build 後長這樣:hyper_main_x7f3a。所以 document.querySelector('.hyper_main') 抓不到東西,要用屬性子字串選擇器:

const hyperMain = document.querySelector('[class*="hyper_main"]');

[class*="..."] 比對的是「class 屬性字串包含這段文字」,對雜湊後綴免疫。整個 updateLayout() 都用這招。

updateLayout():一次手術要動五個器官

把 Terminal 推到右邊,不是只改一個元素。實際的 updateLayout() 要動五個:

function updateLayout(editorOpen) {
  const termLeft = /* 上面的 calc() */;

  // 1. 終端機本體
  const hyperMain = document.querySelector('[class*="hyper_main"]');
  hyperMain.style.setProperty('left', termLeft, 'important');
  hyperMain.style.setProperty('bottom', `${STATUSBAR_HEIGHT}px`, 'important');

  // 2. 分頁列
  const headerEl = document.querySelector('[class*="header_header"]');
  headerEl.style.setProperty('left', `calc(${termLeft} + 1px)`, 'important');

  // 3. 視窗拖曳區(後面詳述,這行救了所有按鈕)
  const windowHeader = document.querySelector('[class*="header_windowHeader"]');
  windowHeader.style.setProperty('left', termLeft, 'important');

  // 4 & 5. 漢堡選單與視窗控制鈕
  document.querySelector('[class*="header_hamburgerMenuLeft"]')
    ?.style.setProperty('left', termLeft, 'important');
  document.querySelector('[class*="header_windowControlsLeft"]')
    ?.style.setProperty('left', termLeft, 'important');
}

漏掉任何一個,畫面就會出現「分頁列飄在 Sidebar 上」「漢堡選單被編輯器蓋住」之類的靈異現象。這也是 DOM 手術式佈局的代價:你接管了佈局,就要接管到底

時機:為什麼要 requestAnimationFrame

updateLayout() 在哪裡被呼叫?開關編輯器 / Vision 面板的瞬間:

componentDidUpdate(prevProps, prevState) {
  const wasOpen = !!prevState.editFile || !!prevState.visionOpen /* ... */;
  const isOpen  = !!this.state.editFile || !!this.state.visionOpen /* ... */;
  if (wasOpen !== isOpen) {
    requestAnimationFrame(() => updateLayout(isOpen));
  }
}

注意那層 requestAnimationFrame。直接在 componentDidUpdate 裡動 DOM 的話,React 這一輪的 commit 可能還沒完全落地,你 querySelector 到的是舊佈局、寫進去的值也可能被同幀稍後的渲染蓋掉:

relayout 時序:requestAnimationFrame 保證順序

requestAnimationFrame 把我們的 DOM 手術排到「下一幀開始前」——React 的活先做完,我們的行內樣式最後蓋章。配合 Day 2 講的 setProperty(..., 'important'),這就是「永遠贏」的兩個條件:優先級最高 + 出手最晚

隱形殺手:-webkit-app-region: drag

佈局做完後,出現一個詭異 bug:Sidebar 頂部的按鈕全部點不到。DevTools 檢查,事件監聽器都在、z-index 沒問題、hover 樣式正常——就是收不到 click。

兇手是 Electron 的視窗拖曳區。Hyper 的 header_windowHeader 是一條 34px 高、橫跨全寬-webkit-app-region: drag 區域(讓無邊框視窗可以拖曳)。落在這條帶子裡的點擊,OS 層直接當成「拖曳視窗」處理,根本不會進到網頁的事件系統——DevTools 當然看不到,它是純粹的事件黑洞:

drag region 吃掉滑鼠事件
解法是雙保險:

// ① updateLayout() 裡:把拖曳區的 left 推到終端機區域
windowHeader.style.setProperty('left', termLeft, 'important');
// ② decorateConfig 注入 CSS:我們的 UI 一律宣告 no-drag
exports.decorateConfig = (config) => Object.assign({}, config, {
  css: `
    ${config.css || ''}
    .hyper_main { bottom: ${STATUSBAR_HEIGHT}px !important; }
    .devterm-sidebar, .devterm-sidebar *,
    .devterm-editor-wrapper, .devterm-editor-wrapper * {
      -webkit-app-region: no-drag !important;
    }
  `,
});

拖曳視窗的功能保留(抓終端機頂部照樣能拖),我們的按鈕全數復活。之後在 Electron 裡遇到「按鈕沒反應但事件系統一切正常」,先懷疑 drag region

順帶一提:decorateConfig 這裡還做了一件事——Windows 上偵測 pwsh.exe(PowerShell 7)存在就設成預設 shell,因為 PS7 的 PSReadLine 才支援預測式自動完成,這是 Day 5 shell 整合的伏筆。

小結

  1. 尺寸用 calc(100vw ...) 表達,resize 交給瀏覽器,插件零 resize listener
  2. CSS Modules 雜湊類名要用 [class*="hyper_main"] 屬性選擇器對付
  3. 接管佈局就要接管到底:一次 relayout 要動五個元素
  4. requestAnimationFrame + 行內 !important = 優先級最高、出手最晚,永遠贏
  5. 按鈕點不到但事件系統正常?先懷疑 -webkit-app-region: drag 事件黑洞

明天(Day 4)進入 Sidebar 的靈魂:檔案樹的遞迴建構(.gitignore 過濾、深度上限),以及用 textarea + highlight.js 疊出一個「夠用就好」的輕量編輯器。



上一篇
# Day 2|Hyper Terminal 插件機制:在 Electron renderer 裡動手術
系列文
讓 Claude 學會看圖:30 天打造 LLM 驅動的機器視覺工作站(MCP × OpenCV × Terminal Plugin)》3
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言